import tkinter as tk  # 导入Tkinter库，用于创建图形用户界面
from tkinter import filedialog, messagebox  # 导入文件对话框和消息框模块
from PIL import Image, ImageTk  # 导入Pillow库，用于处理图像
import cv2  # 导入OpenCV库，用于图像处理
import numpy as np  # 导入NumPy库，用于数组操作
import os  # 导入os模块，用于文件和目录操作
import math  # 导入math模块，用于数学计算

# ========== 图像处理算法 ==========
def to_gray(image):
    # 将图像转换为灰度图，如果已经是灰度图则直接返回
    if len(image.shape) == 2 or (len(image.shape) == 3 and image.shape[2] == 1):
        return image
    return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

def otsu_threshold(gray_img, maxval=255):
    # 使用Otsu方法对灰度图像进行二值化处理
    _, result = cv2.threshold(gray_img, 0, maxval, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return result

def erode(gray_img, ksize=3, iterations=1):
    # 对图像进行腐蚀操作，减少白色区域
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (ksize, ksize))  # 创建矩形核
    return cv2.erode(gray_img, kernel, iterations=iterations)

def dilate(gray_img, ksize=3, iterations=1):
    # 对图像进行膨胀操作，增加白色区域
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (ksize, ksize))  # 创建矩形核
    return cv2.dilate(gray_img, kernel, iterations=iterations)

def morphology_close(gray_img, ksize=3, iterations=1):
    # 对图像进行闭运算，先膨胀后腐蚀，用于填补小孔
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (ksize, ksize))  # 创建矩形核
    return cv2.morphologyEx(gray_img, cv2.MORPH_CLOSE, kernel, iterations=iterations)

def canny_edge(gray_img, th1=50, th2=150):
    # 使用Canny算法进行边缘检测
    return cv2.Canny(gray_img, threshold1=th1, threshold2=th2)

def merge_boxes(boxes, threshold=10):
    # 合并相邻的边界框，减少重复检测
    merged = []
    for box in boxes:
        x, y, w, h = box
        found = False
        for i, (mx, my, mw, mh) in enumerate(merged):
            # 检查两个框是否重叠或接近
            if not (x > mx+mw+threshold or mx > x+w+threshold or y > my+mh+threshold or my > y+h+threshold):
                # 合并两个框
                nx = min(x, mx)
                ny = min(y, my)
                nw = max(x+w, mx+mw) - nx
                nh = max(y+h, my+mh) - ny
                merged[i] = (nx, ny, nw, nh)
                found = True
                break
        if not found:
            merged.append(box)
    return merged

def find_contours_with_merge(gray_img, area_thresh=500, wh_ratio=(0.3, 3), min_wh=15, merge_thresh=10):
    # 查找轮廓并合并相邻的边界框
    contours, _ = cv2.findContours(gray_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 获取外部轮廓
    boxes = []
    for cnt in contours:
        x, y, w, h = cv2.boundingRect(cnt)  # 获取边界框
        area = cv2.contourArea(cnt)  # 计算轮廓面积
        # 根据面积、宽高比和最小宽高过滤轮廓
        if area > area_thresh and wh_ratio[0] < w / h < wh_ratio[1] and w > min_wh and h > min_wh:
            boxes.append((x, y, w, h))
    merged_boxes = merge_boxes(boxes, threshold=merge_thresh)  # 合并相邻框
    if len(gray_img.shape) == 2:
        contour_img = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR)  # 转换为彩色图像
    else:
        contour_img = gray_img.copy()
    for x, y, w, h in merged_boxes:
        cv2.rectangle(contour_img, (x, y), (x + w, y + h), (0, 255, 0), 2)  # 绘制边界框
    return contour_img, merged_boxes

def median_filter(gray_img, ksize=3):
    # 对图像应用中值滤波，去除噪声
    return cv2.medianBlur(gray_img, ksize)

def remove_small_components(binary_img, min_area=100):
    # 移除小的连通区域（噪声点）
    num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_img, connectivity=8)  # 获取连通区域
    output = np.zeros_like(binary_img)  # 创建黑色图像
    for i in range(1, num_labels):  # 遍历连通区域（跳过背景）
        if stats[i, cv2.CC_STAT_AREA] >= min_area:  # 如果面积大于阈值，则保留
            output[labels == i] = 255
    return output

def fill_closed_areas(binary_img):
    # 填充封闭区域
    filled = binary_img.copy()
    contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
    cv2.drawContours(filled, contours, -1, 255, cv2.FILLED)  # 填充轮廓内部
    return filled

# ========== 主界面与功能 ==========
class ImageProcessingApp:
    def __init__(self, root):
        # 初始化应用程序
        self.root = root
        self.root.title("字帖图像处理工具")  # 设置窗口标题
        self.root.geometry("800x600")  # 设置窗口大小
        self.current_image = None  # 当前显示的图像
        self.original_image = None  # 原始图像
        self.crop_region = None  # 裁剪区域
        self.is_drawing = False  # 是否正在绘制裁剪框
        self.rect_start_x = None  # 裁剪框起始x坐标
        self.rect_start_y = None  # 裁剪框起始y坐标
        self.rect_end_x = None  # 裁剪框结束x坐标
        self.rect_end_y = None  # 裁剪框结束y坐标
        self.current_rect = None  # 当前绘制的矩形
        self.char_boxes = []  # 检测到的汉字边界框
        
        # 创建菜单栏
        self.menu_bar = tk.Menu(root)
        
        # 文件菜单
        self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.file_menu.add_command(label="载入图像", command=self.load_image)
        self.file_menu.add_command(label="重置为原图", command=self.reset_to_original)
        self.file_menu.add_separator()
        self.file_menu.add_command(label="退出", command=root.quit)
        
        # 处理菜单
        self.process_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.process_menu.add_command(label="裁剪", command=self.crop_image)
        self.process_menu.add_command(label="原图转灰度图", command=lambda: self.process_image(1))
        self.process_menu.add_command(label="全局阈值处理", command=lambda: self.process_image(2))
        self.process_menu.add_command(label="应用腐蚀操作", command=lambda: self.process_image(3))
        self.process_menu.add_command(label="应用膨胀操作", command=lambda: self.process_image(4))
        self.process_menu.add_command(label="中值滤波去除小白点", command=self.apply_median_filter)
        self.process_menu.add_command(label="应用闭运算", command=lambda: self.process_image(6))
        self.process_menu.add_command(label="填充封闭区域", command=self.fill_closed_areas)
        self.process_menu.add_command(label="移除小连通区域", command=self.remove_small_components)
        self.process_menu.add_command(label="Canny边缘检测", command=lambda: self.process_image(7))
        self.process_menu.add_command(label="轮廓检测", command=self.contour_detect_with_canny)
        self.process_menu.add_command(label="提取汉字", command=self.extract_and_save_chars)
        
        # 添加到菜单栏
        self.menu_bar.add_cascade(label="文件", menu=self.file_menu)
        self.menu_bar.add_cascade(label="处理", menu=self.process_menu)
        
        # 配置菜单栏
        root.config(menu=self.menu_bar)
        
        # 创建画布
        self.canvas = tk.Canvas(root)
        self.canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 绑定鼠标事件
        self.canvas.bind("<Button-1>", self.on_mouse_down)
        self.canvas.bind("<B1-Motion>", self.on_mouse_move)
        self.canvas.bind("<ButtonRelease-1>", self.on_mouse_up)
        
        # 图像显示变量
        self.tk_img = None
        self._img_scale = 1.0
        self._img_pad_x = 0
        self._img_pad_y = 0

    def load_image(self):
        file_path = filedialog.askopenfilename(
            initialdir=".",
            title="选择图像文件",
            filetypes=(("JPG文件", "*.jpg"), ("PNG文件", "*.png"), ("所有文件", "*.*"))
        )
        if not file_path:
            return
        
        try:
            # 使用OpenCV读取图像
            self.original_image = cv2.imread(file_path)
            if self.original_image is None:
                raise Exception("无法识别的图像格式")
            
            self.current_image = self.original_image.copy()
            self.show_image(self.current_image)
            self.status("图像加载成功")
        except Exception as e:
            messagebox.showerror("错误", f"载入图像失败：{str(e)}")

    def status(self, message):
        """显示状态消息"""
        self.root.title(f"字帖图像处理工具 - {message}")

    def show_image(self, img):
        if img is None:
            return
            
        # 将图像转换为RGB格式
        if len(img.shape) == 2:  # 灰度图
            img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
        else:  # 彩色图
            img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        
        # 转换为PIL图像
        pil_img = Image.fromarray(img_rgb)
        
        # 获取画布尺寸
        canvas_w = self.canvas.winfo_width()
        canvas_h = self.canvas.winfo_height()
        
        # 如果画布尺寸为1（尚未显示），使用默认尺寸
        if canvas_w <= 1 or canvas_h <= 1:
            canvas_w = 600
            canvas_h = 400
        
        # 计算缩放比例
        img_h, img_w = img_rgb.shape[:2]
        scale = min(canvas_w / img_w, canvas_h / img_h)
        show_w, show_h = int(img_w * scale), int(img_h * scale)
        pad_x = (canvas_w - show_w) // 2
        pad_y = (canvas_h - show_h) // 2
        
        # 保存缩放和偏移信息（用于裁剪）
        self._img_scale = scale
        self._img_pad_x = pad_x
        self._img_pad_y = pad_y
        
        # 调整图像大小
        pil_img = pil_img.resize((show_w, show_h), Image.LANCZOS)
        
        # 转换为Tkinter图像
        tk_img = ImageTk.PhotoImage(pil_img)
        
        # 更新画布
        self.canvas.delete("all")
        self.canvas.create_image(pad_x, pad_y, anchor=tk.NW, image=tk_img)
        self.canvas.image = tk_img
        self.tk_img = tk_img

    def ask_iterations(self, title="请输入操作次数", prompt="操作次数：", default=1, minval=1, maxval=20, on_change=None):
        input_win = tk.Toplevel(self.root)
        input_win.title(title)
        input_win.geometry("300x120")
        
        tk.Label(input_win, text=prompt).pack(pady=5)
        
        var = tk.IntVar(value=default)
        result = {'value': default}
        
        def scale_callback(val):
            val = int(val)
            result['value'] = val
            if on_change:
                on_change(val)
        
        scale = tk.Scale(input_win, from_=minval, to=maxval, orient=tk.HORIZONTAL, 
                        variable=var, length=200, command=scale_callback)
        scale.pack(pady=5)
        
        def on_ok():
            input_win.destroy()
        
        tk.Button(input_win, text="确定", command=on_ok).pack(pady=5)
        
        input_win.transient(self.root)
        input_win.grab_set()
        self.root.wait_window(input_win)
        
        return result['value']

    def ask_iterations_and_ksize(self, title="闭运算参数选择", prompt1="操作次数：", prompt2="核大小(奇数)：", 
                                default_iter=1, min_iter=1, max_iter=20, 
                                default_ksize=3, min_ksize=3, max_ksize=15, on_change=None):
        input_win = tk.Toplevel(self.root)
        input_win.title(title)
        input_win.geometry("340x200")
        
        tk.Label(input_win, text=prompt1).pack(pady=2)
        
        var_iter = tk.IntVar(value=default_iter)
        var_ksize = tk.IntVar(value=default_ksize)
        result = {'iterations': default_iter, 'ksize': default_ksize}
        
        def scale_callback(val=None):
            iter_val = var_iter.get()
            ksize_val = var_ksize.get()
            # 保证核大小为奇数
            if ksize_val % 2 == 0:
                ksize_val += 1
                var_ksize.set(ksize_val)
            result['iterations'] = iter_val
            result['ksize'] = ksize_val
            if on_change:
                on_change(iter_val, ksize_val)
        
        scale_iter = tk.Scale(input_win, from_=min_iter, to=max_iter, orient=tk.HORIZONTAL, 
                             variable=var_iter, length=260, command=lambda v: scale_callback())
        scale_iter.pack(pady=2)
        
        tk.Label(input_win, text=prompt2).pack(pady=2)
        
        scale_ksize = tk.Scale(input_win, from_=min_ksize, to=max_ksize, orient=tk.HORIZONTAL, 
                              variable=var_ksize, length=260, command=lambda v: scale_callback())
        scale_ksize.pack(pady=2)
        
        def on_ok():
            input_win.destroy()
        
        tk.Button(input_win, text="确定", command=on_ok).pack(pady=5)
        
        input_win.transient(self.root)
        input_win.grab_set()
        self.root.wait_window(input_win)
        
        return result['iterations'], result['ksize']

    def process_image(self, operation_id):
        if self.current_image is None:
            tk.messagebox.showwarning("提示", "请先载入图像！")
            return
            
        processed_img = None
        gray_img = to_gray(self.current_image) if operation_id != 1 else None
        
        try:
            if operation_id == 1:  # 转灰度图
                processed_img = to_gray(self.current_image)
            elif operation_id == 2:  # 全局阈值处理
                processed_img = otsu_threshold(gray_img)
                processed_img = 255 - processed_img  # 反色，使文字为白色
            elif operation_id == 3:  # 腐蚀操作
                # 实时预览腐蚀
                backup = gray_img.copy()
                
                def preview(val):
                    img = erode(backup, iterations=val)
                    self.show_image(img)
                
                iterations = self.ask_iterations(title="腐蚀操作次数", prompt="请选择腐蚀操作次数：", 
                                                default=1, minval=1, maxval=20, on_change=preview)
                processed_img = erode(backup, iterations=iterations)
            elif operation_id == 4:  # 膨胀操作
                # 实时预览膨胀
                backup = gray_img.copy()
                
                def preview(val):
                    img = dilate(backup, iterations=val)
                    self.show_image(img)
                
                iterations = self.ask_iterations(title="膨胀操作次数", prompt="请选择膨胀操作次数：", 
                                                default=1, minval=1, maxval=20, on_change=preview)
                processed_img = dilate(backup, iterations=iterations)
            elif operation_id == 6:  # 闭运算
                # 实时预览闭运算
                backup = gray_img.copy()
                
                def preview(iter_val, ksize_val):
                    bin_img = otsu_threshold(backup)
                    closed = morphology_close(bin_img, ksize=ksize_val, iterations=iter_val)
                    inv = 255 - closed
                    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(inv, connectivity=8)
                    min_area = 30
                    for i in range(1, num_labels):
                        if stats[i, cv2.CC_STAT_AREA] < min_area:
                            inv[labels == i] = 0
                    img = 255 - inv
                    self.show_image(img)
                
                iterations, ksize = self.ask_iterations_and_ksize(
                    title="闭运算参数选择",
                    prompt1="请选择闭运算操作次数：",
                    prompt2="请选择核大小(奇数)：",
                    default_iter=1, min_iter=1, max_iter=20,
                    default_ksize=3, min_ksize=3, max_ksize=15,
                    on_change=preview
                )
                bin_img = otsu_threshold(backup)
                closed = morphology_close(bin_img, ksize=ksize, iterations=iterations)
                inv = 255 - closed
                num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(inv, connectivity=8)
                min_area = 30
                for i in range(1, num_labels):
                    if stats[i, cv2.CC_STAT_AREA] < min_area:
                        inv[labels == i] = 0
                processed_img = 255 - inv
            elif operation_id == 7:  # Canny边缘检测
                processed_img = canny_edge(gray_img)
            else:
                tk.messagebox.showwarning("提示", "未知操作！")
                return
            
            self.current_image = processed_img
            self.show_image(processed_img)
            self.status(f"已应用操作: {self.process_menu.entrycget(operation_id + 1, 'label')}")
        except Exception as e:
            tk.messagebox.showerror("错误", f"处理失败：{str(e)}")

    def crop_image(self):
        if self.current_image is None:
            tk.messagebox.showinfo("错误", "没有图像可裁剪")
            return
            
        if not self.crop_region:
            tk.messagebox.showinfo("错误", "请先用鼠标绘制裁剪区域")
            return
            
        img = self.current_image
        h, w = img.shape[:2]
        scale = getattr(self, '_img_scale', 1.0)
        pad_x = getattr(self, '_img_pad_x', 0)
        pad_y = getattr(self, '_img_pad_y', 0)
        
        # 计算裁剪区域在原图中的坐标
        y0 = int(math.floor((self.rect_start_y - pad_y) / scale))
        y1 = int(math.ceil((self.rect_end_y - pad_y) / scale))
        x0 = int(math.floor((self.rect_start_x - pad_x) / scale))
        x1 = int(math.ceil((self.rect_end_x - pad_x) / scale))
        
        # 确保坐标在有效范围内
        y0 = max(0, min(h-1, y0))
        y1 = max(y0+1, min(h, y1))
        x0 = max(0, min(w-1, x0))
        x1 = max(x0+1, min(w, x1))
        
        # 执行裁剪
        cropped = img[y0:y1, x0:x1]
        self.current_image = cropped
        self.show_image(self.current_image)
        self.crop_region = None
        
        # 删除绘制的矩形
        if self.current_rect:
            self.canvas.delete(self.current_rect)
            self.current_rect = None
        
        # 更新原始图像裁剪区域
        if self.original_image is not None:
            oh, ow = self.original_image.shape[:2]
            y0o = max(0, min(oh-1, y0))
            y1o = max(y0o+1, min(oh, y1))
            x0o = max(0, min(ow-1, x0))
            x1o = max(x0o+1, min(ow, x1))
            self.cropped_original_image = self.original_image[y0o:y1o, x0o:x1o]
        else:
            self.cropped_original_image = None
        
        self.status("图像已裁剪")

    def on_mouse_down(self, event):
        if self.current_image is None:
            return
            
        # 清除之前的矩形
        if self.current_rect and not self.is_drawing:
            self.canvas.delete(self.current_rect)
            self.current_rect = None
            
        # 记录起始坐标
        self.rect_start_x = max(0, min(event.x, self.canvas.winfo_width() - 1))
        self.rect_start_y = max(0, min(event.y, self.canvas.winfo_height() - 1))
        self.is_drawing = True

    def on_mouse_move(self, event):
        if self.is_drawing and self.current_image is not None:
            # 删除之前的矩形
            if self.current_rect:
                self.canvas.delete(self.current_rect)
                
            # 获取当前坐标
            x1 = max(0, min(event.x, self.canvas.winfo_width() - 1))
            y1 = max(0, min(event.y, self.canvas.winfo_height() - 1))
            
            # 绘制新矩形
            self.current_rect = self.canvas.create_rectangle(
                self.rect_start_x, self.rect_start_y, x1, y1, outline="red", width=2)

    def on_mouse_up(self, event):
        if self.is_drawing and self.current_image is not None:
            # 记录结束坐标
            x1 = max(0, min(event.x, self.canvas.winfo_width() - 1))
            y1 = max(0, min(event.y, self.canvas.winfo_height() - 1))
            
            self.rect_end_x = x1
            self.rect_end_y = y1
            self.is_drawing = False
            
            # 检查是否有有效区域
            if abs(self.rect_end_x - self.rect_start_x) > 2 and abs(self.rect_end_y - self.rect_start_y) > 2:
                self.crop_region = (
                    min(self.rect_start_y, self.rect_end_y),
                    max(self.rect_start_y, self.rect_end_y),
                    min(self.rect_start_x, self.rect_end_x),
                    max(self.rect_start_x, self.rect_end_x)
                )
                self.status("已绘制裁剪区域，点击'裁剪'按钮应用")
            else:
                self.crop_region = None
                if self.current_rect:
                    self.canvas.delete(self.current_rect)
                    self.current_rect = None
                self.status("无效的裁剪区域")

    def reset_to_original(self):
        if self.original_image is not None:
            self.current_image = self.original_image.copy()
            self.show_image(self.current_image)
            self.crop_region = None
            self.cropped_original_image = None
            self.status("已重置为原始图像")
        else:
            tk.messagebox.showinfo("提示", "未载入原图，无法重置！")

    def fill_closed_areas(self):
        """填充封闭区域"""
        if self.current_image is None:
            tk.messagebox.showwarning("提示", "请先载入图像！")
            return
            
        # 确保是二值图像
        if len(self.current_image.shape) == 3:
            gray = cv2.cvtColor(self.current_image, cv2.COLOR_BGR2GRAY)
            _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        else:
            _, binary = cv2.threshold(self.current_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        
        # 填充封闭区域
        filled = fill_closed_areas(binary)
        self.current_image = filled
        self.show_image(filled)
        self.status("已填充封闭区域")

    def remove_small_components(self):
        """移除小连通区域"""
        if self.current_image is None:
            tk.messagebox.showwarning("提示", "请先载入图像！")
            return
            
        # 确保是二值图像
        if len(self.current_image.shape) == 3:
            gray = cv2.cvtColor(self.current_image, cv2.COLOR_BGR2GRAY)
            _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        else:
            _, binary = cv2.threshold(self.current_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        
        # 移除小连通区域
        min_area = self.ask_iterations(title="最小面积", prompt="请输入最小面积阈值：", 
                                      default=100, minval=10, maxval=1000)
        cleaned = remove_small_components(binary, min_area)
        self.current_image = cleaned
        self.show_image(cleaned)
        self.status(f"已移除面积小于{min_area}的小区域")

    def contour_detect_with_canny(self):
        if self.current_image is None:
            tk.messagebox.showwarning("提示", "请先载入图像！")
            return
            
        # 确定基础图像
        if hasattr(self, "cropped_original_image") and self.cropped_original_image is not None:
            base_img = self.cropped_original_image.copy()
        else:
            base_img = self.original_image.copy() if self.original_image is not None else self.current_image.copy()
        
        # 转换为灰度图
        if len(self.current_image.shape) == 3:
            gray = cv2.cvtColor(self.current_image, cv2.COLOR_BGR2GRAY)
        else:
            gray = self.current_image.copy()
        
        # 二值化
        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        
        # 形态学操作：闭运算
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=1)
        
        # 膨胀操作
        closed = cv2.dilate(closed, kernel, iterations=1)
        
        # Canny边缘检测
        edges = cv2.Canny(closed, 50, 150)
        
        # 查找轮廓
        contour_img, merged_boxes = find_contours_with_merge(
            edges, 
            area_thresh=400, 
            wh_ratio=(0.3, 3), 
            min_wh=12, 
            merge_thresh=12
        )
        
        self.char_boxes = merged_boxes if merged_boxes else []
        
        # 绘制结果
        if len(base_img.shape) == 2:
            result = cv2.cvtColor(base_img, cv2.COLOR_GRAY2BGR)
        else:
            result = base_img
            
        for x, y, w, h in merged_boxes:
            cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
        
        # 显示结果
        self.current_image = result
        self.show_image(result)
        self.status(f"找到 {len(self.char_boxes)} 个候选汉字区域")

    def extract_and_save_chars(self):
        if self.current_image is None:
            messagebox.showwarning("提示", "请先载入图像！")
            return
            
        # 确定基础图像
        if hasattr(self, "cropped_original_image") and self.cropped_original_image is not None:
            base_img = self.cropped_original_image.copy()
        elif self.original_image is not None:
            base_img = self.original_image.copy()
        else:
            base_img = self.current_image
            
        # 检查是否有检测到的汉字区域
        if not hasattr(self, "char_boxes") or not self.char_boxes:
            messagebox.showinfo("提示", "请先进行轮廓检测，且检测到汉字轮廓！")
            return
            
        # 创建保存目录
        save_dir = os.path.join(os.path.dirname(__file__), 'chars')
        if not os.path.exists(save_dir):
            os.makedirs(save_dir)
            
        count = 0
        
        def sort_boxes(boxes, y_thresh=20):
            """将轮廓按行排序，同一行的按列排序"""
            boxes = sorted(boxes, key=lambda b: b[1])  # 先按y坐标排序
            
            lines = []
            for b in boxes:
                x, y, w, h = b
                found = False
                for line in lines:
                    # 如果当前框与某一行在y方向上的距离小于阈值，则归为同一行
                    if abs(line[0][1] - y) < y_thresh:
                        line.append(b)
                        found = True
                        break
                if not found:
                    lines.append([b])
            
            # 对每一行按x坐标排序
            sorted_boxes = []
            for line in lines:
                line = sorted(line, key=lambda b: b[0])
                sorted_boxes.extend(line)
                
            return sorted_boxes
        
        # 按行排序
        sorted_boxes = sort_boxes(self.char_boxes)
        
        # 提取每个汉字
        for i, (x, y, w, h) in enumerate(sorted_boxes):
            # 过滤掉太小的区域（可能是噪声）
            if w < 15 or h < 15 or w*h < 150:
                continue
                
            # 计算边界坐标
            x0 = max(0, x - 2)  # 扩展一点边界
            y0 = max(0, y - 2)
            x1 = min(base_img.shape[1], x + w + 2)
            y1 = min(base_img.shape[0], y + h + 2)
            
            # 裁剪汉字图像
            char_img = base_img[y0:y1, x0:x1]
            if char_img.size == 0:
                continue
                
            # 转换为灰度图
            if len(char_img.shape) == 3:
                char_gray = cv2.cvtColor(char_img, cv2.COLOR_BGR2GRAY)
            else:
                char_gray = char_img
                
            # 二值化
            _, char_bin = cv2.threshold(char_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
            
            # 创建白色背景的正方形
            size = max(char_bin.shape[0], char_bin.shape[1], 64)
            square = np.ones((size, size), dtype=np.uint8) * 255
            
            # 计算偏移量，使汉字居中
            y_offset = (size - char_bin.shape[0]) // 2
            x_offset = (size - char_bin.shape[1]) // 2
            
            # 将汉字放入正方形中心
            square[y_offset:y_offset+char_bin.shape[0], x_offset:x_offset+char_bin.shape[1]] = char_bin
            
            # 调整大小为64x64
            char_resized = cv2.resize(square, (64, 64), interpolation=cv2.INTER_AREA)
            
            # 保存图像
            save_path = os.path.join(save_dir, f"char_{i+1}.png")
            cv2.imwrite(save_path, char_resized)
            count += 1
            
        messagebox.showinfo("提示", f"已保存 {count} 个汉字图像到 chars 目录！")
        self.status(f"已提取 {count} 个汉字图像")

    def apply_median_filter(self):
        if self.current_image is None:
            tk.messagebox.showwarning("提示", "请先载入图像！")
            return
            
        # 获取核大小
        ksize = self.ask_iterations(title="中值滤波核大小", prompt="请选择奇数核大小(如3,5,7)：", 
                                   default=3, minval=3, maxval=15)
        
        # 验证核大小
        if ksize % 2 == 0:
            ksize += 1  # 确保是奇数
            
        # 应用滤波
        gray_img = to_gray(self.current_image)
        filtered = median_filter(gray_img, ksize=ksize)
        self.current_image = filtered
        self.show_image(filtered)
        self.status(f"已应用中值滤波 (核大小={ksize})")

# ========== 程序入口 ==========
if __name__ == "__main__":
    root = tk.Tk()
    app = ImageProcessingApp(root)
    root.mainloop()